<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Console\Input;

/**
 * ArgvInput represents an input coming from the CLI arguments.
 *
 * Usage:
 *
 * $input = new ArgvInput();
 *
 * By default, the `$_SERVER['argv']` array is used for the input values.
 *
 * This can be overridden by explicitly passing the input values in the constructor:
 *
 * $input = new ArgvInput($_SERVER['argv']);
 *
 * If you pass it yourself, don't forget that the first element of the array
 * is the name of the running program.
 *
 * When passing an argument to the constructor, be sure that it respects
 * the same rules as the argv one. It's almost always better to use the
 * `StringInput` when you want to provide your own input.
 *
 * @author Fabien Potencier <fabien@symfony.com>
 *
 * @see http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
 * @see http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html#tag_12_02
 *
 * @api
 */
class ArgvInput extends Input {
	private $tokens;
	private $parsed;
	
	/**
	 * Constructor.
	 *
	 * @param array           $argv An array of parameters from the CLI (in the argv format)
	 * @param InputDefinition $definition A InputDefinition instance
	 *
	 * @api
	 */
	public function __construct(array $argv = null, InputDefinition $definition = null) {
		if( null === $argv ) {
			$argv = $_SERVER['argv'];
		}
		
		// strip the program name
		array_shift( $argv );
		
		$this->tokens = $argv;
		
		parent::__construct( $definition );
	}
	
	protected function setTokens(array $tokens) {
		$this->tokens = $tokens;
	}
	
	/**
	 * Processes command line arguments.
	 */
	protected function parse() {
		$this->parsed = $this->tokens;
		while ( null !== $token = array_shift( $this->parsed ) ) {
			if( '--' === substr( $token, 0, 2 ) ) {
				$this->parseLongOption( $token );
			} elseif( '-' === $token[0] ) {
				$this->parseShortOption( $token );
			} else {
				$this->parseArgument( $token );
			}
		}
	}
	
	/**
	 * Parses a short option.
	 *
	 * @param string $token The current token.
	 */
	private function parseShortOption($token) {
		$name = substr( $token, 1 );
		
		if( strlen( $name ) > 1 ) {
			if( $this->definition->hasShortcut( $name[0] ) && $this->definition->getOptionForShortcut( $name[0] )->acceptValue() ) {
				// an option with a value (with no space)
				$this->addShortOption( $name[0], substr( $name, 1 ) );
			} else {
				$this->parseShortOptionSet( $name );
			}
		} else {
			$this->addShortOption( $name, null );
		}
	}
	
	/**
	 * Parses a short option set.
	 *
	 * @param string $name The current token
	 *
	 * @throws \RuntimeException When option given doesn't exist
	 */
	private function parseShortOptionSet($name) {
		$len = strlen( $name );
		for( $i = 0; $i < $len; $i++ ) {
			if( !$this->definition->hasShortcut( $name[$i] ) ) {
				throw new \RuntimeException( sprintf( 'The "-%s" option does not exist.', $name[$i] ) );
			}
			
			$option = $this->definition->getOptionForShortcut( $name[$i] );
			if( $option->acceptValue() ) {
				$this->addLongOption( $option->getName(), $i === $len - 1 ? null : substr( $name, $i + 1 ) );
				
				break;
			} else {
				$this->addLongOption( $option->getName(), true );
			}
		}
	}
	
	/**
	 * Parses a long option.
	 *
	 * @param string $token The current token
	 */
	private function parseLongOption($token) {
		$name = substr( $token, 2 );
		
		if( false !== $pos = strpos( $name, '=' ) ) {
			$this->addLongOption( substr( $name, 0, $pos ), substr( $name, $pos + 1 ) );
		} else {
			$this->addLongOption( $name, null );
		}
	}
	
	/**
	 * Parses an argument.
	 *
	 * @param string $token The current token
	 *
	 * @throws \RuntimeException When too many arguments are given
	 */
	private function parseArgument($token) {
		$c = count( $this->arguments );
		
		// if input is expecting another argument, add it
		if( $this->definition->hasArgument( $c ) ) {
			$arg = $this->definition->getArgument( $c );
			$this->arguments[$arg->getName()] = $arg->isArray() ? array( $token ) : $token;
		
		// if last argument isArray(), append token to last argument
		} elseif( $this->definition->hasArgument( $c - 1 ) && $this->definition->getArgument( $c - 1 )->isArray() ) {
			$arg = $this->definition->getArgument( $c - 1 );
			$this->arguments[$arg->getName()][] = $token;
		
		// unexpected argument
		} else {
			throw new \RuntimeException( 'Too many arguments.' );
		}
	}
	
	/**
	 * Adds a short option value.
	 *
	 * @param string $shortcut The short option key
	 * @param mixed  $value    The value for the option
	 *
	 * @throws \RuntimeException When option given doesn't exist
	 */
	private function addShortOption($shortcut, $value) {
		if( !$this->definition->hasShortcut( $shortcut ) ) {
			throw new \RuntimeException( sprintf( 'The "-%s" option does not exist.', $shortcut ) );
		}
		
		$this->addLongOption( $this->definition->getOptionForShortcut( $shortcut )->getName(), $value );
	}
	
	/**
	 * Adds a long option value.
	 *
	 * @param string $name  The long option key
	 * @param mixed  $value The value for the option
	 *
	 * @throws \RuntimeException When option given doesn't exist
	 */
	private function addLongOption($name, $value) {
		if( !$this->definition->hasOption( $name ) ) {
			throw new \RuntimeException( sprintf( 'The "--%s" option does not exist.', $name ) );
		}
		
		$option = $this->definition->getOption( $name );
		
		if( null === $value && $option->acceptValue() ) {
			// if option accepts an optional or mandatory argument
			// let's see if there is one provided
			$next = array_shift( $this->parsed );
			if( '-' !== $next[0] ) {
				$value = $next;
			} else {
				array_unshift( $this->parsed, $next );
			}
		}
		
		if( null === $value ) {
			if( $option->isValueRequired() ) {
				throw new \RuntimeException( sprintf( 'The "--%s" option requires a value.', $name ) );
			}
			
			$value = $option->isValueOptional() ? $option->getDefault() : true;
		}
		
		$this->options[$name] = $value;
	}
	
	/**
	 * Returns the first argument from the raw parameters (not parsed).
	 *
	 * @return string The value of the first argument or null otherwise
	 */
	public function getFirstArgument() {
		foreach( $this->tokens as $token ) {
			if( $token && '-' === $token[0] ) {
				continue;
			}
			
			return $token;
		}
	}
	
	/**
	 * Returns true if the raw parameters (not parsed) contains a value.
	 *
	 * This method is to be used to introspect the input parameters
	 * before it has been validated. It must be used carefully.
	 *
	 * @param string|array $values The value(s) to look for in the raw parameters (can be an array)
	 *
	 * @return Boolean true if the value is contained in the raw parameters
	 */
	public function hasParameterOption($values) {
		$values = ( array ) $values;
		
		foreach( $this->tokens as $v ) {
			if( in_array( $v, $values ) ) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Returns the value of a raw option (not parsed).
	 *
	 * This method is to be used to introspect the input parameters
	 * before it has been validated. It must be used carefully.
	 *
	 * @param string|array $values The value(s) to look for in the raw parameters (can be an array)
	 * @param mixed $default The default value to return if no result is found
	 * @return mixed The option value
	 */
	public function getParameterOption($values, $default = false) {
		$values = ( array ) $values;
		
		$tokens = $this->tokens;
		while ( $token = array_shift( $tokens ) ) {
			foreach( $values as $value ) {
				if( 0 === strpos( $token, $value ) ) {
					if( false !== $pos = strpos( $token, '=' ) ) {
						return substr( $token, $pos + 1 );
					}
					
					return array_shift( $tokens );
				}
			}
		}
		
		return $default;
	}
}
